﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;

namespace LowLevelGraphics.VideoFormats
{
    public class AVI
    {
        /// <summary>
        /// File access modes. 
        /// </summary>
        /// 
        [Flags]
        public enum OpenFileMode
        {
            Read = 0x00000000,
            Write = 0x00000001,
            ReadWrite = 0x00000002,
            ShareCompat = 0x00000000,
            ShareExclusive = 0x00000010,
            ShareDenyWrite = 0x00000020,
            ShareDenyRead = 0x00000030,
            ShareDenyNone = 0x00000040,
            Parse = 0x00000100,
            Delete = 0x00000200,
            Verify = 0x00000400,
            Cancel = 0x00000800,
            Create = 0x00001000,
            Prompt = 0x00002000,
            Exist = 0x00004000,
            Reopen = 0x00008000
        }


        /// <summary>
        /// Return the address of a decompressed video frame. 
        /// </summary>
        /// 
        /// <param name="getFrameObject">Pointer to a GetFrame object.</param>
        /// <param name="position">Position, in samples, within the stream of the desired frame.</param>
        /// 
        /// <returns>Returns a pointer to the frame data if successful or NULL otherwise.</returns>
        /// 
        [DllImport("avifil32.dll")]
        public static extern IntPtr AVIStreamGetFrame(
            IntPtr getFrameObject,
            int position);

                /// <summary>
        /// Copy a block of memory.
        /// </summary>
        /// 
        /// <param name="dst">Destination pointer.</param>
        /// <param name="src">Source pointer.</param>
        /// <param name="count">Memory block's length to copy.</param>
        /// 
        /// <returns>Return's the value of <b>dst</b> - pointer to destination.</returns>
        /// 
        [DllImport( "ntdll.dll", CallingConvention = CallingConvention.Cdecl )]
        public static extern int memcpy(
            int dst,
            int src,
            int count );


        /// <summary>
        /// Prepare to decompress video frames from the specified video stream
        /// </summary>
        /// 
        /// <param name="streamHandler">Pointer to the video stream used as the video source.</param>
        /// <param name="wantedFormat">Pointer to a structure that defines the desired video format. Specify NULL to use a default format.</param>
        /// 
        /// <returns>Returns an object that can be used with the <see cref="AVIStreamGetFrame"/> function.</returns>
        /// 
        [DllImport("avifil32.dll")]
        public static extern IntPtr AVIStreamGetFrameOpen(
            IntPtr streamHandler,
            ref BITMAPINFOHEADER wantedFormat);


        [StructLayout(LayoutKind.Sequential)]
        public struct RECT
        {
            private int _Left;
            private int _Top;
            private int _Right;
            private int _Bottom;

            public RECT(RECT Rectangle)
                : this(Rectangle.Left, Rectangle.Top, Rectangle.Right, Rectangle.Bottom)
            {
            }

            public RECT(int Left, int Top, int Right, int Bottom)
            {
                _Left = Left;
                _Top = Top;
                _Right = Right;
                _Bottom = Bottom;
            }

            public int X
            {
                get { return _Left; }
                set { _Left = value; }
            }

            public int Y
            {
                get { return _Top; }
                set { _Top = value; }
            }

            public int Left
            {
                get { return _Left; }
                set { _Left = value; }
            }

            public int Top
            {
                get { return _Top; }
                set { _Top = value; }
            }

            public int Right
            {
                get { return _Right; }
                set { _Right = value; }
            }

            public int Bottom
            {
                get { return _Bottom; }
                set { _Bottom = value; }
            }

            public int Height
            {
                get { return _Bottom - _Top; }
                set { _Bottom = value + _Top; }
            }

            public int Width
            {
                get { return _Right - _Left; }
                set { _Right = value + _Left; }
            }

            public Point Location
            {
                get { return new Point(Left, Top); }
                set
                {
                    _Left = value.X;
                    _Top = value.Y;
                }
            }

            public Size Size
            {
                get { return new Size(Width, Height); }
                set
                {
                    _Right = value.Width + _Left;
                    _Bottom = value.Height + _Top;
                }
            }

            public static implicit operator Rectangle(RECT Rectangle)
            {
                return new Rectangle(Rectangle.Left, Rectangle.Top, Rectangle.Width, Rectangle.Height);
            }

            public static implicit operator RECT(Rectangle Rectangle)
            {
                return new RECT(Rectangle.Left, Rectangle.Top, Rectangle.Right, Rectangle.Bottom);
            }

            public static bool operator ==(RECT Rectangle1, RECT Rectangle2)
            {
                return Rectangle1.Equals(Rectangle2);
            }

            public static bool operator !=(RECT Rectangle1, RECT Rectangle2)
            {
                return !Rectangle1.Equals(Rectangle2);
            }

            public override string ToString()
            {
                return "{Left: " + _Left + "; " + "Top: " + _Top + "; Right: " + _Right + "; Bottom: " + _Bottom + "}";
            }

            public override int GetHashCode()
            {
                return ToString().GetHashCode();
            }

            public bool Equals(RECT Rectangle)
            {
                return Rectangle.Left == _Left && Rectangle.Top == _Top && Rectangle.Right == _Right && Rectangle.Bottom == _Bottom;
            }

            public override bool Equals(object Object)
            {
                if (Object is RECT)
                {
                    return Equals((RECT)Object);
                }
                else if (Object is Rectangle)
                {
                    return Equals(new RECT((Rectangle)Object));
                }

                return false;
            }
        }

        [DllImport("avifil32.dll")]
        public static extern int AVIFileOpen(out IntPtr ppfile, string szFile, uint mode, IntPtr pclsidHandler);

        /// <summary>
        /// Releases resources used to decompress video frames.
        /// </summary>
        /// 
        /// <param name="getFrameObject">Handle returned from the <see cref="AVIStreamGetFrameOpen(IntPtr,int)"/> function.</param>
        /// 
        /// <returns>Returns zero if successful or an error otherwise.</returns>
        /// 
        [DllImport("avifil32.dll")]
        public static extern int AVIStreamGetFrameClose(
            IntPtr getFrameObject);

        [DllImport("avifil32.dll", EntryPoint = "AVIStreamInfo")]
        static extern int AVIGetStreamInfo(IntPtr pavi, out AVISTREAMINFO psi, int lSize);

        [DllImport("avifil32.dll")]
        static extern void AVIFileExit();

        [DllImport("avifil32.dll")]
        static extern void AVIFileInit();

        [DllImport("avifil32.dll", SetLastError = true)]
        public static extern int AVIFileOpenW(ref int ppfile, [MarshalAs(UnmanagedType.LPWStr)]string szFile, int uMode, int lpHandler);

        [DllImport("avifil32.dll")]
        static extern uint AVIFileRelease(IntPtr pfile);


        [DllImport("avifil32.dll")]
        static extern uint AVIStreamLength(IntPtr pavi);

        /// <summary>
        /// Structure, which contains information for a single stream .
        /// </summary>
        /// 
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 1)]
        public struct AVISTREAMINFO
        {
            /// <summary>
            /// Four-character code indicating the stream type.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int type;

            /// <summary>
            /// Four-character code of the compressor handler that will compress this video stream when it is saved.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int handler;

            /// <summary>
            /// Applicable flags for the stream.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int flags;

            /// <summary>
            /// Capability flags; currently unused.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int Capabilities;

            /// <summary>
            /// Priority of the stream.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I2)]
            public short priority;

            /// <summary>
            /// Language of the stream.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I2)]
            public short language;

            /// <summary>
            /// Time scale applicable for the stream.
            /// </summary>
            /// 
            /// <remarks>Dividing <b>rate</b> by <b>scale</b> gives the playback rate in number of samples per second.</remarks>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int scale;

            /// <summary>
            /// Rate in an integer format.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int rate;

            /// <summary>
            /// Sample number of the first frame of the AVI file.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int start;

            /// <summary>
            /// Length of this stream.
            /// </summary>
            /// 
            /// <remarks>The units are defined by <b>rate</b> and <b>scale</b>.</remarks>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int length;

            /// <summary>
            /// Audio skew. This member specifies how much to skew the audio data ahead of the video frames in interleaved files.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int initialFrames;

            /// <summary>
            /// Recommended buffer size, in bytes, for the stream.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int suggestedBufferSize;

            /// <summary>
            /// Quality indicator of the video data in the stream.
            /// </summary>
            /// 
            /// <remarks>Quality is represented as a number between 0 and 10,000.</remarks>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int quality;

            /// <summary>
            /// Size, in bytes, of a single data sample.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int sampleSize;

            /// <summary>
            /// Dimensions of the video destination rectangle.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.Struct, SizeConst = 16)]
            public RECT rectFrame;

            /// <summary>
            /// Number of times the stream has been edited.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int editCount;

            /// <summary>
            /// Number of times the stream format has changed.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int formatChangeCount;

            /// <summary>
            /// Description of the stream.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
            public string name;
        }

        [DllImport("avifil32.dll")]
        static extern int AVIStreamRead(IntPtr pavi, int lStart, int lSamples, IntPtr lpBuffer, int cbBuffer, out int plBytes, out int plSamples);

        [DllImport("avifil32.dll")]
        static extern int AVIStreamReadFormat(IntPtr pavi, int lPos, out WAVEFORMATEX lpFormat, out int lpcbFormat);

        [StructLayout(LayoutKind.Sequential)]
        struct WAVEFORMATEX
        {
            public ushort wFormatTag;
            public ushort nChannels;
            public uint nSamplesPerSec;
            public uint nAvgBytesPerSec;
            public ushort nBlockAlign;
            public ushort wBitsPerSample;
            public ushort cbSize;
        }

        /// <summary>
        /// Get stream interface that is associated with a specified AVI file
        /// </summary>
        /// 
        /// <param name="aviHandler">Handler to an open AVI file.</param>
        /// <param name="streamHandler">Stream interface.</param>
        /// <param name="streamType">Stream type to open.</param>
        /// <param name="streamNumner">Count of the stream type. Identifies which occurrence of the specified stream type to access. </param>
        /// 
        /// <returns></returns>
        /// 
        [DllImport("avifil32.dll")]
        public static extern int AVIFileGetStream(
            IntPtr aviHandler,
            out IntPtr streamHandler,
            int streamType,
            int streamNumner);


        [DllImport("avifil32.dll")]
        static extern int AVIStreamRelease(IntPtr pavi);

        /// <summary>
        /// Obtain stream header information.
        /// </summary>
        /// 
        /// <param name="streamHandler">Handle to an open stream.</param>
        /// <param name="streamInfo">Pointer to a structure to contain the stream information.</param>
        /// <param name="infoSize">Size, in bytes, of the structure used for <b>streamInfo</b>.</param>
        /// 
        /// <returns>Returns zero if successful or an error otherwise.</returns>
        /// 
        [DllImport("avifil32.dll", CharSet = CharSet.Unicode)]
        public static extern int AVIStreamInfo(
            IntPtr streamHandler,
            ref AVISTREAMINFO streamInfo,
            int infoSize);


        /// <summary>
        /// Structure, which contains information about the dimensions and color format of a DIB.
        /// </summary>
        /// 
        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        public struct BITMAPINFOHEADER
        {
            /// <summary>
            /// Specifies the number of bytes required by the structure.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int size;

            /// <summary>
            /// Specifies the width of the bitmap, in pixels.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int width;

            /// <summary>
            /// Specifies the height of the bitmap, in pixels.
            /// </summary>
            /// 
            /// <remarks>If <b>heigh</b>t is positive, the bitmap is a bottom-up DIB and its origin is
            /// the lower-left corner. If <b>height</b> is negative, the bitmap is a top-down DIB and its
            /// origin is the upper-left corner.</remarks>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int height;

            /// <summary>
            /// Specifies the number of planes for the target device. This value must be set to 1.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I2)]
            public short planes;

            /// <summary>
            /// Specifies the number of bits-per-pixel.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I2)]
            public short bitCount;

            /// <summary>
            /// Specifies the type of compression for a compressed bottom-up bitmap (top-down DIBs cannot be compressed).
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int compression;

            /// <summary>
            /// Specifies the size, in bytes, of the image.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int sizeImage;

            /// <summary>
            /// Specifies the horizontal resolution, in pixels-per-meter, of the target device for the bitmap.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int xPelsPerMeter;

            /// <summary>
            /// Specifies the vertical resolution, in pixels-per-meter, of the target device for the bitmap.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int yPelsPerMeter;

            /// <summary>
            /// Specifies the number of color indexes in the color table that are actually used by the bitmap.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int colorsUsed;

            /// <summary>
            /// Specifies the number of color indexes that are required for displaying the bitmap.
            /// </summary>
            /// 
            [MarshalAs(UnmanagedType.I4)]
            public int colorsImportant;
        }

        private IntPtr file;
        private IntPtr stream;
        private IntPtr getFrame;


        private int width;
        private int height;
        private int position;
        private int start;
        private int length;
        private float rate;
        private string codec;
        object sync = false;

        /// <summary>
        /// Get the position
        /// </summary>
        public int Position
        {
            get { return position; }
        }

        /// <summary>
        /// Open Avi File
        /// </summary>
        /// <param name="sFileName"></param>
        public void Open(string sFileName)
        {
            // close previous file
            Close();

            bool success = false;

            try
            {
                lock (sync)
                {
                    // open AVI file
                    if (AVIFileOpen(out file, sFileName, (int)OpenFileMode.ShareDenyWrite, IntPtr.Zero) != 0)
                        throw new System.IO.IOException("Failed opening the specified AVI file.");

                    // get first video stream
                    if (AVIFileGetStream(file, out stream, mmioFOURCC("vids"), 0) != 0)
                        throw new ApplicationException("Failed getting video stream.");

                    // get stream info
                    AVISTREAMINFO info = new AVISTREAMINFO();
                    AVIStreamInfo(stream, ref info, Marshal.SizeOf(info));

                    width = info.rectFrame.Right;
                    height = info.rectFrame.Bottom;
                    position = info.start;
                    start = info.start;
                    length = info.length;
                    rate = (float)info.rate / (float)info.scale;
                    codec = decode_mmioFOURCC(info.handler);

                    // prepare decompressor
                    BITMAPINFOHEADER bitmapInfoHeader = new BITMAPINFOHEADER();

                    bitmapInfoHeader.size = Marshal.SizeOf(bitmapInfoHeader.GetType());
                    bitmapInfoHeader.width = width;
                    bitmapInfoHeader.height = height;
                    bitmapInfoHeader.planes = 1;
                    bitmapInfoHeader.bitCount = 24;
                    bitmapInfoHeader.compression = 0; // BI_RGB

                    // get frame object
                    if ((getFrame = AVIStreamGetFrameOpen(stream, ref bitmapInfoHeader)) == IntPtr.Zero)
                    {
                        bitmapInfoHeader.height = -height;

                        if ((getFrame = AVIStreamGetFrameOpen(stream, ref bitmapInfoHeader)) == IntPtr.Zero)
                            throw new ApplicationException("Failed initializing decompressor.");
                    }

                    success = true;
                }
            }
            finally
            {
                if (!success)
                {
                    Close();
                }
            }
        }

        /// <summary>
        /// Gets the next Frame
        /// </summary>
        /// <returns></returns>
        public Bitmap GetNextFrame()
        {
            lock ( sync )
            {
                if ( file == IntPtr.Zero )
                {
                    throw new System.IO.IOException( "Cannot read video frames since video file is not open." );
                }

                // get frame at specified position
                IntPtr DIB = AVIStreamGetFrame( getFrame, position );
                if ( DIB == IntPtr.Zero )
                    throw new ApplicationException( "Failed getting frame." );

                BITMAPINFOHEADER bitmapInfoHeader;

                // copy BITMAPINFOHEADER from unmanaged memory
                bitmapInfoHeader = (BITMAPINFOHEADER) Marshal.PtrToStructure( DIB, typeof( BITMAPINFOHEADER ) );

                // create new bitmap
                Bitmap image = new Bitmap( width, height, PixelFormat.Format24bppRgb );

                // lock bitmap data
                BitmapData imageData = image.LockBits(
                    new Rectangle( 0, 0, width, height ),
                    ImageLockMode.ReadWrite,
                    PixelFormat.Format24bppRgb );

                // copy image data
                int srcStride = imageData.Stride;
                int dstStride = imageData.Stride;

                // check image direction
                if ( bitmapInfoHeader.height > 0 )
                {
                    // it`s a bottom-top image
                    int dst = imageData.Scan0.ToInt32( ) + dstStride * ( height - 1 );
                    int src = DIB.ToInt32( ) + Marshal.SizeOf( typeof( BITMAPINFOHEADER ) );

                    for ( int y = 0; y < height; y++ )
                    {
                        memcpy( dst, src, srcStride );
                        dst -= dstStride;
                        src += srcStride;
                    }
                }
                else
                {
                    // it`s a top bootom image
                    int dst = imageData.Scan0.ToInt32( );
                    int src = DIB.ToInt32( ) + Marshal.SizeOf( typeof( BITMAPINFOHEADER ) );

                    // copy the whole image
                    memcpy( dst, src, srcStride * height );
                }

                // unlock bitmap data
                image.UnlockBits( imageData );

                // move position to the next frame
                position++;

                return image;
            }
        }



        /// <summary>
        /// Close video file.
        /// </summary>
        /// 
        public void Close()
        {
            lock (sync)
            {
                // release get frame object
                if (getFrame != IntPtr.Zero)
                {
                    AVIStreamGetFrameClose(getFrame);
                    getFrame = IntPtr.Zero;
                }

                // release stream
                if (stream != IntPtr.Zero)
                {
                    AVIStreamRelease(stream);
                    stream = IntPtr.Zero;
                }

                // release file
                if (file != IntPtr.Zero)
                {
                    AVIFileRelease(file);
                    file = IntPtr.Zero;
                }
            }
        }


        /// <summary>
        /// .NET replacement of mmioFOURCC macros. Converts four characters to code.
        /// </summary>
        /// 
        /// <param name="str">Four characters string.</param>
        /// 
        /// <returns>Returns the code created from provided characters.</returns>
        /// 
        public static int mmioFOURCC(string str)
        {
            return (
                ((int)(byte)(str[0])) |
                ((int)(byte)(str[1]) << 8) |
                ((int)(byte)(str[2]) << 16) |
                ((int)(byte)(str[3]) << 24));
        }

        /// <summary>
        /// Inverse to <see cref="mmioFOURCC"/>. Converts code to fout characters string.
        /// </summary>
        /// 
        /// <param name="code">Code to convert.</param>
        /// 
        /// <returns>Returns four characters string.</returns>
        /// 
        public static string decode_mmioFOURCC(int code)
        {
            char[] chs = new char[4];

            for (int i = 0; i < 4; i++)
            {
                chs[i] = (char)(byte)((code >> (i << 3)) & 0xFF);
                if (!char.IsLetterOrDigit(chs[i]))
                    chs[i] = ' ';
            }
            return new string(chs);
        }
    }
}
